/*
 * JFugue - API for Music Programming
 * Copyright (C) 2003-2008  David Koelle
 *
 * http://www.jfugue.org 
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *  
 */

package org.jfugue;

import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.Sequence;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.SysexMessage;
import javax.sound.midi.Track;

/**
 * Parses MIDI data, whether from a file, a connected device, or some other
 * stream.
 * 
 * @version 4.0.3 - A Note event with 0 duration is now sent when a note is
 *          first encountered
 */
public final class MidiParser extends Parser {
	long[][] tempNoteRegistry = new long[16][255];
	byte[][] tempNoteAttackRegistry = new byte[16][255];
	int tempo;
	private static final int DEFAULT_TEMPO = 120;

	public MidiParser() {
		this.tempo = DEFAULT_TEMPO;

		// Create a two dimensional array of bytes [ track, note ] - when a
		// NoteOn event is found,
		// populate the proper spot in the array with the note's start time.
		// When a NoteOff event
		// is found, new Time and Note objects are constructed and added to the
		// composition
		for (int m = 0; m < 16; m++) {
			for (int n = 0; n < 255; n++) {
				tempNoteRegistry[m][n] = 0L;
				tempNoteAttackRegistry[m][n] = (byte) 0;
			}
		}
	}

	/**
	 * Parses a <code>Sequence</code> and fires events to subscribed
	 * <code>ParserListener</code> interfaces. As the Sequence is parsed, events
	 * are sent to <code>ParserListener</code> interfaces, which are responsible
	 * for doing something interesting with the music data, such as adding notes
	 * to a pattern.
	 *
	 * @param sequence
	 *            the <code>Sequence</code> to parse
	 * @throws RuntimeException
	 *             if there is an error parsing the pattern
	 */
	public void parse(Sequence sequence) {
		this.tempo = DEFAULT_TEMPO;

		// Get the MIDI tracks from the sequence. Expect a maximum of 16 tracks.
		Track[] tracks = sequence.getTracks();

		// Compute the size of this adventure for the ParserProgressListener
		long totalCount = 0;
		long counter = 0;
		for (byte i = 0; i < tracks.length; i++) {
			totalCount += tracks[i].size();
		}

		// And now to parse the MIDI!
		for (int t = 0; t < tracks.length; t++) {
			int trackSize = tracks[t].size();
			if (trackSize > 0) {
				fireVoiceEvent(new Voice((byte) t));

				for (int ev = 0; ev < trackSize; ev++) {
					counter++;
					fireProgressReported("Parsing MIDI...", counter,
							totalCount);

					MidiEvent event = tracks[t].get(ev);
					MidiMessage message = event.getMessage();

					trace("Message received: ", message);
					parse(message, event.getTick());
				}
			}
		}
	}

	/**
	 * Delegator method that calls specific parsers depending on the type of
	 * MidiMessage passed in.
	 * 
	 * @param message
	 *            the message to parse
	 * @param timestamp
	 *            the time at which the message was encountered in this track
	 */
	public void parse(MidiMessage message, long timestamp) {
		if (message instanceof ShortMessage) {
			parseShortMessage((ShortMessage) message, timestamp);
		} else if (message instanceof SysexMessage) {
			parseSysexMessage((SysexMessage) message, timestamp);
		} else if (message instanceof MetaMessage) {
			parseMetaMessage((MetaMessage) message, timestamp);
		}
	}

	/**
	 * Parses instances of ShortMessage.
	 * 
	 * @param message
	 *            The message to parse
	 * @param timestamp
	 *            the time at which the message was encountered in this track
	 */
	private void parseShortMessage(ShortMessage message, long timestamp) {
		int track = message.getChannel();

		switch (message.getCommand()) {
		case ShortMessage.PROGRAM_CHANGE: // 0xC0, 192
			trace("Program change to ", message.getData1());
			Instrument instrument = new Instrument((byte) message.getData1());
			fireTimeEvent(new Time(timestamp));
			fireVoiceEvent(new Voice((byte) track));
			fireInstrumentEvent(instrument);
			break;

		case ShortMessage.CONTROL_CHANGE: // 0xB0, 176
			trace("Controller change to ", message.getData1(), ", value = ",
					message.getData2());
			Controller controller = new Controller((byte) message.getData1(),
					(byte) message.getData2());
			fireTimeEvent(new Time(timestamp));
			fireVoiceEvent(new Voice((byte) track));
			fireControllerEvent(controller);
			break;
		case ShortMessage.NOTE_ON: // 0x90, 144
			if (message.getData2() == 0) {
				// A velocity of zero in a note-on event is a note-off event
				noteOffEvent(timestamp, track, message.getData1(),
						message.getData2());
			} else {
				noteOnEvent(timestamp, track, message.getData1(),
						message.getData2());
			}
			break;
		case ShortMessage.NOTE_OFF: // 0x80, 128
			noteOffEvent(timestamp, track, message.getData1(),
					message.getData2());
			break;
		case ShortMessage.CHANNEL_PRESSURE: // 0xD0, 208
			trace("Channel pressure, pressure = ", message.getData1());
			ChannelPressure pressure = new ChannelPressure(
					(byte) message.getData1());
			fireTimeEvent(new Time(timestamp));
			fireVoiceEvent(new Voice((byte) track));
			fireChannelPressureEvent(pressure);
			break;
		case ShortMessage.POLY_PRESSURE: // 0xA0, 128
			trace("Poly pressure on key ", message.getData1(), ", pressure = ",
					message.getData2());
			PolyphonicPressure poly = new PolyphonicPressure(
					(byte) message.getData1(), (byte) message.getData2());
			fireTimeEvent(new Time(timestamp));
			fireVoiceEvent(new Voice((byte) track));
			firePolyphonicPressureEvent(poly);
			break;
		case ShortMessage.PITCH_BEND: // 0xE0, 224
			trace("Pitch Bend, data1= ", message.getData1(), ", data2= ",
					message.getData2());
			PitchBend bend = new PitchBend((byte) message.getData1(),
					(byte) message.getData2());
			fireTimeEvent(new Time(timestamp));
			fireVoiceEvent(new Voice((byte) track));
			firePitchBendEvent(bend);
			break;
		default:
			trace("Unparsed message: ", message.getCommand());
			break;
		}
	}

	private void noteOnEvent(long timestamp, int track, int data1, int data2) {
		trace("Note on ", data1, " - attack is ", data2);
		tempNoteRegistry[track][data1] = timestamp;
		tempNoteAttackRegistry[track][data1] = (byte) data2;

		// Added 9/27/2008 - fire a Note with duration 0 to signify a that a
		// Note was pressed
		Note note = new Note((byte) data1, 0);
		note.setDecimalDuration(0);
		note.setAttackVelocity((byte) data2);
		fireNoteEvent(note);
	}

	private void noteOffEvent(long timestamp, int track, int data1, int data2) {
		long time = tempNoteRegistry[track][data1];
		trace("Note off ", data1, " - decay is ", data2, ". Duration is ",
				(timestamp - time));

		fireTimeEvent(new Time(time));
		fireVoiceEvent(new Voice((byte) track));
		Note note = new Note((byte) data1, timestamp - time);
		note.setDecimalDuration(
				(timestamp - time) / (this.tempo * 4.0D));
		note.setAttackVelocity(tempNoteAttackRegistry[track][data1]);
		note.setDecayVelocity((byte) data2);
		fireNoteEvent(note);
		tempNoteRegistry[track][data1] = 0L;
	}

	/**
	 * Parses instances of SysexMessage.
	 * 
	 * @param message
	 *            The message to parse
	 * @param timestamp
	 *            the time at which the message was encountered in this track
	 */
	private void parseSysexMessage(SysexMessage message, long timestamp) {
		// Nothing to do - JFugue doesn't use sysex messages
		trace("SysexMessage received but not parsed by JFugue (doesn't use them)");
	}

	/**
	 * Parses instances of MetaMessage.
	 * 
	 * @param message
	 *            The message to parse
	 * @param timestamp
	 *            the time at which the message was encountered in this track
	 */
	private void parseMetaMessage(MetaMessage message, long timestamp) {
		switch (message.getType()) {
		case 0x51:
			parseTempo(message, timestamp);
			break;
		case 0x59:
			break; // Even though we care about Key Signatures, we don't want to
					// read one in from a MIDI file,
					// because the notes that we'll receive will already be
					// adjusted for the key signature.
					// MIDI's Key Signature is more about notating sheet music
					// that influencing the played notes.
		default:
			break;
		}
		// Nothing to do - JFugue doesn't use sysex messages
		trace("MetaMessage received but not parsed by JFugue (doesn't use them)");
	}

	private void parseTempo(MetaMessage message, long timestamp) {
		int beatsPerMinute = TimeFactor.parseMicrosecondsPerBeat(message,
				timestamp);
		trace("Tempo Event, bpm = ", beatsPerMinute);
		fireTimeEvent(new Time(timestamp));
		fireTempoEvent(new Tempo(beatsPerMinute));
		this.tempo = beatsPerMinute;
	}
}
